#include <iostream>
#include <vector>
#include <fstream>
#include <algorithm>
#include <filesystem>
#include <cstring>
#include "zip_utils.h"

using namespace std;

// Core compression function
VecData compress(const VecData &raw) {
    // Compressing
    VecData compressed;
    auto it = raw.cbegin();
    auto unrep_start = raw.cend(); // While not repeating, unrep_start is at end()
    bool save_rep = false;
    bool save_unrep = false;

    while (it < raw.cend()) {
        // Find first byte unequal to current
        auto rep_end = find_if(it, raw.cend(), [it](unsigned char b) {
            return b != *it;
        });

        // Check if repeated data is long enough
        auto rep_length = distance(it, rep_end);

        if (rep_length > 3) {
            save_rep = true;
            if (unrep_start != raw.cend())
                save_unrep = true;

            // If >= 127, trunc at 127 and regard the following data as new sequence
            if (rep_length >= 127) {
                rep_end = it + 127;
                rep_length = 127;
            }
        }

        // If >= 127, trunc at 127 and regard the following data as new sequence
        if (distance(unrep_start, it) >= 127)
            save_unrep = true;

        // Compress unrepeated data
        if (save_unrep) {
            compressed.push_back(distance(unrep_start, it));
            compressed.insert(compressed.cend(), unrep_start, it);
            unrep_start = raw.cend(); // Reset to end()
        }

        // Compress repeated data
        if (save_rep) {
            compressed.push_back(0x80 + rep_length);
            compressed.push_back(*it);
            it = rep_end;
        }

        if (!save_rep && !save_unrep) {
            // If repeating, set to the repetition start
            if (unrep_start == raw.cend())
                unrep_start = it;
            ++it;
        }

        save_rep = false;
        save_unrep = false;
    }

    // If the last is unrepeated data
    if (unrep_start != raw.cend()) {
        compressed.push_back(distance(unrep_start, raw.cend()));
        compressed.insert(compressed.cend(), unrep_start, raw.cend());
    }

    return compressed;
}

// Read file, compress, and load; get compressed size additionally
void read_load(ofstream &output, const string &file_path, size_t &raw_size, size_t &comp_size,
               const string &root) {
    ifstream file(file_path, ios::binary);
    if (!file.is_open()) {
        throw runtime_error("Failed opening file : " + file_path);
    }

    // Read and compress file data
    VecData data((istreambuf_iterator<char>(file)), istreambuf_iterator<char>());
    file.close();
    VecData compressed = compress(data);

    // Record metadata
    string relpath = get_relpath(file_path, root);
    FileEntry fe = {static_cast<unsigned int>(relpath.length()),
                    static_cast<unsigned int>(compressed.size())};

    output.write(reinterpret_cast<char *>(&fe), sizeof(FileEntry));
    output << relpath;
    output.write(reinterpret_cast<const char *>(compressed.data()),
                 static_cast<streamsize>(compressed.size()));

    // Calculate sizes
    raw_size += data.size();
    comp_size += compressed.size() + sizeof(FileEntry) + relpath.size();
}

// Get relative path from the full path
string get_relpath(const string &path, const string &root) {
    if (root.empty())
        return filesystem::path(path).filename().string();
    else
        return filesystem::relative(filesystem::path(path), root).string();
}
